<?php 

namespace pmvc\mvc;

use pmvc\util\StringUtil;
use pmvc\util\ClassUtil;
use pmvc\log\Log;
use pmvc\ApplicationContext;
use pmvc\XmlApplicationContext;
use pmvc\FileSystemClassLoader;
use Exception;

class Dispatcher {

	const REDIRECT_VIEWNAME_PREFIX	= "redirect:";
	const ERROR_VIEWNAME_PREFIX		= "errorController:";

	private $applicationContext	= NULL;

    /**
     * Creates the Dispatcher.
     * @param ApplicationContext $applicationContext
     */
    public function __construct(ApplicationContext $applicationContext) {
    	$this->applicationContext = $applicationContext;
    }

	/**
	 * Convienience function for invoking the MVC dispatcher.
	 * TODO: explain how it sets up the environment
	 * @param mixed $context application context (string, array or ObjectFactory)
	 * @param array $classPath the classpath
	 */
	public static function factory($context=NULL, $classPath=NULL) {
	
		// set default configuration
		if ($context==NULL) {
			$context = dirname($_SERVER["SCRIPT_FILENAME"])."/WEB-INF/applicationContext.xml";
		}

		// set default class path
		if ($classPath==NULL) {
			$classPath = Array(dirname($_SERVER["SCRIPT_FILENAME"])."/WEB-INF/classes");
			$hLib = opendir(dirname($_SERVER["SCRIPT_FILENAME"])."/WEB-INF/lib");
			while (FALSE !== ($dir = readdir($hLib))) {
				if ($dir=="." || $dir=="..") { continue; }
				$dir = dirname($_SERVER["SCRIPT_FILENAME"])."/WEB-INF/lib/".$dir;
				if (is_dir($dir)) {
					$classPath []= $dir;
				}
			}
		}
		if (count($classPath)>0) {
			ClassUtil::addClassLoader(new FileSystemClassLoader($classPath));
		}
		
		// create app context
		$app = new XmlApplicationContext($context);
		$app->initialize();
	
		// create the dispatcher and return it
		return new Dispatcher($app);
	}

    /**
     * Returns the URL that was requested.
     * @return string the URL
     */
    public function getApplicationContext() {
    	return $this->applicationContext;
    }

    /**
     * Dispatches the request.
     */
    public function dispatch($url=NULL) {
    	Log::trace("Dispatching request");
    	$dispatchStartTime = microtime(TRUE);
   
    	// start session
    	session_start();

    	// make sure we have a url
    	if ($url==NULL) {
    		$url = $_SERVER["REQUEST_URI"];
    	}

    	// create request and response
    	$request	= new Request($this, $url);
    	$response	= new Response($this);

    	// run request wrappers
    	if (!$this->dispatch_RequestWrapper($request, $response, TRUE)) {
	    	Log::trace("Dispatcher took {0} milliseconds", (microtime(TRUE)-$dispatchStartTime)*1000);
			return;
    	}

    	// run controller
    	$viewName = $this->dispatch_Controller($request, $response);
    	
    	// run view handler
    	if (!empty($viewName)) {
    		
    		// check for redirect
    		if (StringUtil::startsWith($viewName, self::REDIRECT_VIEWNAME_PREFIX)) {
    			$toUrl = substr($viewName, strlen(self::REDIRECT_VIEWNAME_PREFIX));
    			$toUrl = ModelUtil::stringReplace($toUrl, $response->getModel());
    			if (!StringUtil::startsWith($toUrl, "http", TRUE)
    				&& !StringUtil::startsWith($toUrl, "/")) {
    				$toUrl = $request->getBaseUrl()."/".$toUrl;
    			}
    			Log::debug("Redirecting to {0}", $toUrl);
    			$response->sendRedirect($toUrl);
    			
    		} else {
    			$this->dispatch_viewHandler($request, $response, $viewName);
    		}

    	// no view
    	} else {
			Log::debug(
		        "No view name returned from controller, assuming controller handled it");
    	}
   
    	// run request wrappers
    	$this->dispatch_RequestWrapper($request, $response, FALSE);

    	// print the output buffer
    	if (!$response->isCommitted()) {
	    	$response->send();
    	}

    	// log elapsed time
    	Log::trace("Dispatcher took {0} milliseconds", (microtime(TRUE)-$dispatchStartTime)*1000);
    }

    /**
     * Dispatches RequestWrappers.
     * @param bool $beforeRequest
     */
    private function dispatch_RequestWrapper(Request $request, Response $response, $beforeRequest) {
		Log::trace("Running RequestWrappers");
    	
        // get request mapper handler mapping
    	$wrapperMappings = $this->applicationContext->getObjectsOfType("pmvc\mvc\RequestWrapperMapping");
    	foreach ($wrapperMappings as $wrapperMapping) {
    		$requestWrappers = $wrapperMapping->getRequestWrappers($request);
    		if (!empty($requestWrappers) && count($requestWrappers)>0) {
	    		foreach ($requestWrappers as $wrapper) {
    				Log::trace("RequestWrapper: {0}", get_class($wrapper));
					if ($beforeRequest) {
						if ($wrapper->beforeRequest($request, $response)===FALSE) {
							Log::debug(
						        "Request wrapper canceled further execution: {0}", get_class($wrapper));
							return FALSE;
						}
					} else {
						$wrapper->afterRequest($request, $response);
					}
	    		}
    		}
    	}
    	return TRUE;
    }

    /**
     * Dispatches {@link Controller}s.
     */
    private function dispatch_Controller(Request $request, Response $response) {
    	
        // get controller mappings
    	$controllerMappings = $this->applicationContext->getObjectsOfType("pmvc\mvc\ControllerMapping");
    	// get controller for requested URL
    	$controller = $this->getController($controllerMappings, $request->getRequestedUrl(), $request);

    	if (!isset($controller)) {
    		// get a 404 controller
    		$controller = $this->getController($controllerMappings, self::ERROR_VIEWNAME_PREFIX.'404', $request);
    		if (!isset($controller)) {
    			$controller = new \pmvc\mvc\controller\FourOhFourController();
    		}
    	}
    	Log::trace("Controller: {0}", get_class($controller));
    	
    	try {
	    	return $controller->handleRequest($request, $response);
	    	
    	} catch(Exception $e) {
    		// get a 500 controller
    		$controller = $this->getController($controllerMappings, self::ERROR_VIEWNAME_PREFIX.'500', $request);
    		$request->setAttribute("exception", $e);
    		if (!isset($controller)) {
    			$controller = new \pmvc\mvc\controller\ExceptionController();
    		}
    		Log::error("Exception during dispatch, deferring to controller: {0}", get_class($controller));
    		return $controller->handleRequest($request, $response);
    	}
    }
    
    /**
     * Gets a controller for a URL.
     * @param Array $controllerMappings
     * @param $url the requested url
     * @param string $url
     */
    private function getController(Array $controllerMappings, $url, Request $request) {
        foreach ($controllerMappings as $controllerMapping) {
    		$controller = $controllerMapping->getController($url, $request);
    		if (isset($controller)) {
    			return $controller;
    		}
    	}
    	return NULL;
    }

    /**
     * Dispatches {@link ViewHandler}s.
     */
    private function dispatch_viewHandler(Request $request, Response $response, $viewName) {
    	
        // get view handler mappings
    	$viewHandlerMappings = $this->applicationContext->getObjectsOfType("pmvc\mvc\ViewHandlerMapping");
    	$viewHandler = NULL;
    	foreach ($viewHandlerMappings as $viewHandlerMapping) {
    		$viewHandler = $viewHandlerMapping->getViewHandler($viewName, $request);
    		if (!empty($viewHandler)) {
    			break;
    		}
    	}

    	// invoke the view handler
    	if (!empty($viewHandler)) {
    		Log::trace("ViewHandler: {0}", get_class($viewHandler));
    		return $viewHandler->renderView($request, $response, $viewName);
    		
    	} else {
        	Log::warn(
        		"No ViewHandler configured for view {0}", 
        		$viewName);
    	}
    }
}

?>